package com.thaiopensource.datatype.xsd;

import java.util.HashMap;
import java.util.Map;

import org.relaxng.datatype.Datatype;
import org.relaxng.datatype.DatatypeBuilder;
import org.relaxng.datatype.DatatypeException;
import org.relaxng.datatype.DatatypeLibrary;

import com.thaiopensource.datatype.xsd.regex.RegexEngine;
import com.thaiopensource.datatype.xsd.regex.RegexSyntaxException;

public class DatatypeLibraryImpl implements DatatypeLibrary
{
  private final Map <String, DatatypeBase> typeMap = new HashMap <String, DatatypeBase> ();
  private final RegexEngine regexEngine;

  static private final String LONG_MAX = "9223372036854775807";
  static private final String LONG_MIN = "-9223372036854775808";
  static private final String INT_MAX = "2147483647";
  static private final String INT_MIN = "-2147483648";
  static private final String SHORT_MAX = "32767";
  static private final String SHORT_MIN = "-32768";
  static private final String BYTE_MAX = "127";
  static private final String BYTE_MIN = "-128";

  static private final String UNSIGNED_LONG_MAX = "18446744073709551615";
  static private final String UNSIGNED_INT_MAX = "4294967295";
  static private final String UNSIGNED_SHORT_MAX = "65535";
  static private final String UNSIGNED_BYTE_MAX = "255";
  // Follow RFC 3066 syntax.
  static private final String LANGUAGE_PATTERN = "[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*";

  public DatatypeLibraryImpl (final RegexEngine regexEngine)
  {
    this.regexEngine = regexEngine;
    typeMap.put ("string", new StringDatatype ());
    typeMap.put ("normalizedString", new CdataDatatype ());
    typeMap.put ("token", new TokenDatatype ());
    typeMap.put ("boolean", new BooleanDatatype ());

    final DatatypeBase decimalType = new DecimalDatatype ();
    typeMap.put ("decimal", decimalType);
    final DatatypeBase integerType = new IntegerRestrictDatatype (decimalType);
    typeMap.put ("integer", integerType);
    typeMap.put ("nonPositiveInteger", restrictMax (integerType, "0"));
    typeMap.put ("negativeInteger", restrictMax (integerType, "-1"));
    typeMap.put ("long", restrictMax (restrictMin (integerType, LONG_MIN), LONG_MAX));
    typeMap.put ("int", restrictMax (restrictMin (integerType, INT_MIN), INT_MAX));
    typeMap.put ("short", restrictMax (restrictMin (integerType, SHORT_MIN), SHORT_MAX));
    typeMap.put ("byte", restrictMax (restrictMin (integerType, BYTE_MIN), BYTE_MAX));
    final DatatypeBase nonNegativeIntegerType = restrictMin (integerType, "0");
    typeMap.put ("nonNegativeInteger", nonNegativeIntegerType);
    typeMap.put ("unsignedLong", restrictMax (nonNegativeIntegerType, UNSIGNED_LONG_MAX));
    typeMap.put ("unsignedInt", restrictMax (nonNegativeIntegerType, UNSIGNED_INT_MAX));
    typeMap.put ("unsignedShort", restrictMax (nonNegativeIntegerType, UNSIGNED_SHORT_MAX));
    typeMap.put ("unsignedByte", restrictMax (nonNegativeIntegerType, UNSIGNED_BYTE_MAX));
    typeMap.put ("positiveInteger", restrictMin (integerType, "1"));
    typeMap.put ("double", new DoubleDatatype ());
    typeMap.put ("float", new FloatDatatype ());

    typeMap.put ("Name", new NameDatatype ());
    typeMap.put ("QName", new QNameDatatype ());

    final DatatypeBase ncNameType = new NCNameDatatype ();
    typeMap.put ("NCName", ncNameType);

    final DatatypeBase nmtokenDatatype = new NmtokenDatatype ();
    typeMap.put ("NMTOKEN", nmtokenDatatype);
    typeMap.put ("NMTOKENS", list (nmtokenDatatype));

    typeMap.put ("ID", new IdDatatype ());
    final DatatypeBase idrefType = new IdrefDatatype ();
    typeMap.put ("IDREF", idrefType);
    typeMap.put ("IDREFS", list (idrefType));

    typeMap.put ("NOTATION", new QNameDatatype ());

    typeMap.put ("base64Binary", new Base64BinaryDatatype ());
    typeMap.put ("hexBinary", new HexBinaryDatatype ());
    typeMap.put ("anyURI", new AnyUriDatatype ());
    typeMap.put ("language", new RegexDatatype (LANGUAGE_PATTERN)
    {
      @Override
      String getLexicalSpaceKey ()
      {
        return "language";
      }
    });

    typeMap.put ("dateTime", new DateTimeDatatype ("Y-M-DTt"));
    typeMap.put ("time", new DateTimeDatatype ("t"));
    typeMap.put ("date", new DateTimeDatatype ("Y-M-D"));
    typeMap.put ("gYearMonth", new DateTimeDatatype ("Y-M"));
    typeMap.put ("gYear", new DateTimeDatatype ("Y"));
    typeMap.put ("gMonthDay", new DateTimeDatatype ("--M-D"));
    typeMap.put ("gDay", new DateTimeDatatype ("---D"));
    typeMap.put ("gMonth", new DateTimeDatatype ("--M"));

    final DatatypeBase entityType = new EntityDatatype ();
    typeMap.put ("ENTITY", entityType);
    typeMap.put ("ENTITIES", list (entityType));
    // Partially implemented
    typeMap.put ("duration", new DurationDatatype ());
  }

  public DatatypeBuilder createDatatypeBuilder (final String localName) throws DatatypeException
  {
    final DatatypeBase base = typeMap.get (localName);
    if (base == null)
      throw new DatatypeException ();
    if (base instanceof RegexDatatype)
    {
      try
      {
        ((RegexDatatype) base).compile (getRegexEngine ());
      }
      catch (final RegexSyntaxException e)
      {
        throw new DatatypeException (DatatypeBuilderImpl.localizer.message ("regex_internal_error", localName));
      }
    }
    return new DatatypeBuilderImpl (this, base);
  }

  RegexEngine getRegexEngine () throws DatatypeException
  {
    if (regexEngine == null)
      throw new DatatypeException (DatatypeBuilderImpl.localizer.message ("regex_impl_not_found"));
    return regexEngine;
  }

  private static DatatypeBase restrictMax (final DatatypeBase base, final String limit)
  {
    try
    {
      return new MaxInclusiveRestrictDatatype (base, base.getValue (limit, null), limit);
    }
    catch (final DatatypeException e)
    {
      throw new AssertionError ();
    }
  }

  private static DatatypeBase restrictMin (final DatatypeBase base, final String limit)
  {
    try
    {
      return new MinInclusiveRestrictDatatype (base, base.getValue (limit, null), limit);
    }
    catch (final DatatypeException e)
    {
      throw new AssertionError ();
    }
  }

  private static DatatypeBase list (final DatatypeBase base)
  {
    return new MinLengthRestrictDatatype (new ListDatatype (base), 1);
  }

  public Datatype createDatatype (final String type) throws DatatypeException
  {
    return createDatatypeBuilder (type).createDatatype ();
  }
}
